<?php

namespace app\utils;

use app\exception\FunctionNotFoundException;
use Closure;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use ArrayAccess;
use IteratorAggregate;
use Countable;
use app\exception\ClassNotFoundException;
use ReflectionFunctionAbstract;
use ReflectionNamedType;
use think\helper\Str;
use Traversable;

class Container implements ContainerInterface, ArrayAccess, IteratorAggregate, Countable
{
    /**
     * 当前容器对象实例
     * @var Closure|Container
     */
    protected static $instance;

    /**
     * 保存在容器中的对象实例
     * @var array
     */
    protected $instances = [];

    /**
     * 容器绑定标识
     * @var array
     */
    protected $bind = [];


    protected $invokeCallback = [];


    /**
     * 获取容器对象实例
     * @return mixed
     */
    public static function getInstance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }

        if (static::$instance instanceof \Closure) {
            return (static::$instance)();
        }

        return static::$instance;
    }

    /**
     * 设置容器对象实例
     * @param $instance
     * @return void
     */
    public static function setInstance($instance): void
    {
        static::$instance = $instance;
    }

    /**
     * 判断容器中是否存在对象实例
     * @param string $abstract 类名或者标识
     * @return bool
     */
    public function exist(string $abstract): bool
    {
        $abstract = $this->getAlias($abstract);
        return isset($this->instances[$abstract]);
    }

    /**
     * 获取容器中的对象实例
     * @template T
     * @param string|class-string<T> $abstract 类名或者标识
     * @return T|object
     */
    public function get(string $abstract)
    {
        if ($this->has($abstract)) {
            return $this->make($abstract);
        }
        throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
    }

    /**
     * 判断容器中是否存在类及标识
     * @param string $abstract 类名或者标识
     * @return bool
     */
    public function has(string $abstract): bool
    {
        return $this->bound($abstract);
    }

    public function bound(string $abstract): bool
    {
        return isset($this->bind[$abstract]) || isset($this->instances[$abstract]);
    }


    public function __get($name)
    {
        return $this->get($name);
    }

    public function __isset($name)
    {
        return $this->exist($name);
    }

    public function offsetExists(mixed $key): bool
    {
        return $this->exist($key);
    }

    public function offsetGet(mixed $key): mixed
    {
        return $this->make($key);
    }

    public function offsetSet(mixed $offset, mixed $value)
    {
        // TODO: Implement offsetSet() method.
    }

    public function offsetUnset(mixed $offset)
    {
        // TODO: Implement offsetUnset() method.
    }

    public function getIterator()
    {
        // TODO: Implement getIterator() method.
    }

    public function count()
    {
        return count($this->instances);
    }

    /**
     * 调用反射执行类的方法 支持参数绑定
     * @param string $class 类
     * @param array $vars 类实例的参数
     * @return object|string|null
     * @throws \ReflectionException
     */
    public function invokeClass(string $class, array $vars = [])
    {
        try {
            $reflect = new \ReflectionClass($class);
        } catch (\ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
        }

        if ($reflect->hasMethod('__make')) {  // 如果这个类存在 __make方法
            $method = $reflect->getMethod('__make');
            if ($method->isPublic() && $method->isStatic()) {
                $args = $this->bindParams($method, $vars);
                $object = $method->invokeArgs(null, $args);
                $this->invokeAfter($class, $object);
                return $object;
            }
        }

        // 获取构造方法反射
        $constructor = $reflect->getConstructor();
        $args = $constructor ? $this->bindParams($constructor, $vars) : [];

        $object = $reflect->newInstanceArgs($args); // 创建新实例
        $this->invokeAfter($class, $object);

        return $object;
    }

    /**
     * @param string|Closure $function
     * @param array $vars
     * @return mixed
     */
    protected function invokeFunction(string|Closure $function, array $vars = [])
    {
        try {
            $reflect = new \ReflectionFunction($function);
        } catch (\ReflectionException $e) {
            throw new FunctionNotFoundException("function not exists: {$function}()", $function, $e);
        }
        $args = $this->bindParams($reflect, $vars);

        return $function(...$args);
    }


    /**
     * 获取绑定参数
     * @param ReflectionFunctionAbstract $reflect
     * @param array $vars
     * @return array
     */
    protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array
    {
        // 如果方法或者函数没有参数，返回空值回去
        if ($reflect->getNumberOfParameters() == 0) {
            return [];
        }

        reset($vars);
        // 获取数组的键名 一维数组：1 二维数组：0
        $type = key($vars) === 0 ? 1 : 0;
        $args = [];

        // 获取这个方法或函数的参数信息
        $params = $reflect->getParameters();

        foreach ($params as $param) {
            $name = $param->getName();        // 参数名称
            $snakeName = Str::snake($name);   // 把参数改下划线格式
            $reflectType = $param->getType(); // 参数类型

            if ($param->isVariadic()) { // 如果参数是可变参数
                return array_merge($args, array_values($vars));
            } elseif ($reflectType instanceof ReflectionNamedType && $reflectType->isBuiltin() === false) { // 如何这个参数类型是一个类，接口，trait
                $args[] = $this->getObjectParam($reflectType->getName(), $vars);
            } elseif ($type == 1 && !empty($vars)) {
                $args[] = array_shift($vars);
            } elseif ($type == 0 && array_key_exists($name, $vars)) {
                $args[] = $vars[$name];
            } elseif ($type == 0 && array_key_exists($snakeName, $vars)) {
                $args[] = $vars[$snakeName];
            } elseif ($param->isDefaultValueAvailable()) { // 如果参数有默认值，取默认值
                $args[] = $param->getDefaultValue();
            } else {
                throw new InvalidArgumentException('method param miss:' . $name);
            }
        }
        return $args;
    }

    /**
     * 获取对象类型的参数值
     * @param string $className
     * @param array $vars
     * @return mixed
     */
    protected function getObjectParam(string $className, array &$vars = [])
    {
        $params = $vars;
        $value = array_shift($params);

        if ($value instanceof $className) {
            $result = $value;
            array_shift($vars);
        } else {
            $result = $this->make($className);
        }
        return $result;
    }

    /**
     * 创建类的实例 已经存在则直接获取
     * @template T
     * @param string|class-string<T> $className 类名或者标识
     * @param array $vars 参数
     * @param bool $newInstance 是否生成新的实例
     * @return T|object
     */
    public function make(string $className, array $vars = [], bool $newInstance = false)
    {
        $className = $this->getAlias($className);

        if (isset($this->instances[$className]) && !$newInstance) {
            return $this->instances[$className];
        }

        if (isset($this->bind[$className]) && $this->bind[$className] instanceof Closure) {
            $object = $this->invokeFunction($this->bind[$className], $vars);
        } else {
            $object = $this->invokeClass($className, $vars);
        }

        if (!$newInstance) {
            $this->instances[$className] = $object;
        }

        return $object;
    }


    public function getAlias(string $name): string
    {
        if (isset($this->bind[$name])) {
            $bind = $this->bind[$name];

            if (is_string($bind)) {
                return $this->getAlias($bind);
            }
        }

        return $name;
    }

    protected function invokeAfter(string $class, $object): void
    {
        if (isset($this->invokeCallback['*'])) {
            foreach ($this->invokeCallback['*'] as $callback) {
                $callback($object, $this);
            }
        }

        if (isset($this->invokeCallback[$class])) {
            foreach ($this->invokeCallback[$class] as $callback) {
                $callback($object, $this);
            }
        }
    }
}
